﻿using System;
using System.Linq;

namespace Outfish {

	/// <summary>
	/// represents and HtmlDocument
	/// </summary>
	public class HtmlDocument : WebDocument {
	
		#region constructor

		public HtmlDocument( string htmlSource )
			:this( htmlSource, HtmlParseOptions.None, null )
		{}
	
		public HtmlDocument( string htmlSource, HtmlParseOptions options )
			:this(htmlSource,options,null)
		{}

		public HtmlDocument( string htmlSource, HtmlParseOptions options, Action<HtmlNode> nodeAdded )
			:base(htmlSource)
		{
			if( htmlSource == null ){ throw new ArgumentNullException("htmlSource"); }
			this.NodeAdded = nodeAdded;
			this.DocumentNode = this.ParseHtml( options );
		}


		#endregion
	
		public LineManager LineManager{
			get{	return _lineManager = _lineManager ?? new LineManager(this.Source); }
		}

		#region private Parse methods

		ContainerNode ParseHtml(HtmlParseOptions options){

			ContainerNode root = null;
			ContainerNode curNode = null;
			int curIndex = this.Source.IndexOf('<');
			if( curIndex < 0  ){ throw new ArgumentException("No root node found."); }
			
			// setup root node
			HeaderFooter rootHeader = ParseHeaderFooter( this.Source, curIndex, options );
			if( rootHeader.Type == HeaderFooterType.Tail ){
				throw new Exception("root node cannot be ending tag.");
			}
			root = new ContainerNode( this, rootHeader ); // root
			if( rootHeader.Type != HeaderFooterType.Single ){ curNode = root; }
			curIndex = rootHeader.End;

			// 
			while( curIndex < this.Source.Length && curNode != null ){
			
				if( this.Source[curIndex] != '<' ){
					// text
					this.DoText(curNode, ref curIndex, options);
				} else if( this.MatchesAt( "<!--",curIndex) ){
					// comment
					curIndex = this.DoComment( curNode, curIndex );
				} else {
					// normal tag
					curNode = this.DoTag(curNode, ref curIndex, options);
				}
			
			}// while

//			this._profiler.ReportTimes();

			return root;
			
		}

		void DoText(ContainerNode curNode, ref int curIndex, HtmlParseOptions options){
			// parse text node
			TextNode textNode = new TextNode( this.Source, curIndex );
			// add to current node
			if( (options & HtmlParseOptions.KeepEmptyText)!=0
				|| ! textNode.IsWhiteSpace
			){
				curNode.AddChild( textNode );
			}
			// move to end
			curIndex = textNode.End;
		}

		bool MatchesAt( string needle, int index ){
		
			//return this.Source.IndexOf( needle, index) == index; // old way - searches entire string
		
			// new way... checks only at current positoin
			if( index + needle.Length > this.Source.Length ){ return false; }
			for(int i=0;i<needle.Length;++i){
				if( this.Source[index+i] != needle[i] ){ return false; }
			}
			return true;
		}

		int DoComment(ContainerNode curNode, int index){
			CommentNode commentNode = new CommentNode( this.Source, index );	// parse text node
			curNode.AddChild( commentNode );	// add
			return commentNode.End;		// move to end
		}

		// returns the new Current Node
		ContainerNode DoTag(ContainerNode curNode,ref int curIndex, HtmlParseOptions options){
		
			int index = curIndex; // get it
		
//			_profiler.Wrap( "DoTag", () => {
		
				// parse header/footer
				HeaderFooter headerFooter = ParseHeaderFooter( this.Source, index, options );
				
				if( headerFooter.Type != HeaderFooterType.Tail ){
					curNode = DoStarterTag( curNode, headerFooter );
				} else {
					curNode = DoTailTag( curNode, headerFooter, options );
				}
				
				index = headerFooter.End;
			
//			});
			curIndex = index;
			return curNode;
			
		}
	
		// returns the new current node
		ContainerNode DoStarterTag( ContainerNode cur, HeaderFooter tag ){

			// Head & Single
			// new node
			ContainerNode n = new ContainerNode( this, tag );
	
			// create parent-child relationship
			cur.AddChild( n );
	
			// force some node types to be single
			bool isSingle = tag.Type == HeaderFooterType.Single
				|| _emptyTags.Contains( tag.Name );
			// if forced-empty node has a tailtag, it will be ignored
	
			// !! Not doing anything special about illegally embeded nodes such as 
			//		<p>first paragraph <p> new paragraph, close previous <p> another paragraph

			if( !isSingle ){ cur = n; }

			return cur;

		}

		// returns the new current node
		ContainerNode DoTailTag( ContainerNode cur, HeaderFooter tag, HtmlParseOptions options ){
	
//			return _profiler.Wrap("DoTailTag", () => {
	
			// find self or parent with matching header tag
			ContainerNode searchNode = cur;
			while( searchNode != null && searchNode.Header.Name.ToLower() != tag.Name ){
				searchNode = searchNode.ParentNode;
			}
			
			// if no matching head found
			if( searchNode == null ) {
				// don't register this tail tag at all
				return cur;
			}
			
			// add to current node
			searchNode.Footer = tag;
			
			if( _noInnerTagNodes.Contains( tag.Name.ToLower() ) ){

				// convert whatever children happened to be parsed, into a single text node
				// used primarily to fix <script> nodes that have < and > in them
				searchNode.ChildNodes.Clear();

				// expand script tags only if they ask for it.
				if( (options & HtmlParseOptions.ExpandScriptNodes) != 0 ){
					// _profiler.Wrap("parseJs", () => {
					JavaScriptDocument.BuildStructureTree( searchNode );
					// });
				} else {
					searchNode.AddChild( new TextNode( searchNode.Source, searchNode.Header.End, searchNode.Footer.Begin ) );
				}
			}
			
			// return current node to parent
			return searchNode.ParentNode;

//			});

		}

		HeaderFooter ParseHeaderFooter(string src, int begin, HtmlParseOptions options){

//			return _profiler.Wrap("ParseHeaderFooter", () => {

			if( src == null ){ throw new ArgumentNullException("src"); }
			if( begin >= src.Length ){ 
				throw new ArgumentException("begin is out of bounds."); 
			}
			if( src[begin] != '<' ){ throw new ArgumentException("first char must be a '<'"); }
	
			// skip first char because:
			//  must be good and
			//  don't want our detector to stop on it
			int end = begin + 1;
			
			while( end < src.Length	// runs out of string
				&& src[end] != '<'  // finds new tag
				&& src[end-1] != '>'// properly closes
			){
				++end;
			}

			// grab sub string for conveni
			//string sub = src.Substring( begin, end-begin );

			// name
			int nameBegin = begin+1; if( nameBegin<end && src[nameBegin] == '/' ){ nameBegin++; }
			while( nameBegin < end && char.IsWhiteSpace( src[nameBegin] ) ){ nameBegin++; }
			int nameEnd = nameBegin;
			while( nameEnd < end && (char.IsLetterOrDigit( src[nameEnd] ) || src[nameEnd] == '_') ){ nameEnd++; }
			string name = src.Substring(nameBegin,nameEnd-nameBegin);
			
			// var regex = new Regex(@"\G</?\s*(\w+)");
			// string name = regex.Match(src,begin).Groups[1].Value;
		
			HeaderFooterType type;
			// Set tag type
			if( end-begin>1 && src[begin+1] == '/' ){
				type = HeaderFooterType.Tail;
			} else if( end-begin>=2 && src[ end-2 ] == '/' ){
				type = HeaderFooterType.Single;
			} else {
				type = HeaderFooterType.Head;
			}
			
			return new HeaderFooter(src,begin,end,name,type);
			
//			});
			
		}

		#endregion

		#region private field

		LineManager _lineManager;

		static string[] _emptyTags = new string[]{
			 "br"
			,"hr"
			,"img"
			,"input"
		};
		// This is a no-win situration. - read more about it here:  http://www.cs.tut.fi/~jkorpela/html/empty.html
		// This is the best I can do...
	
		static string[] _noInnerTagNodes = new string[]{ "script" };
	
		#endregion
		
	}
	



	[Flags]
	public enum HtmlParseOptions{ None = 0
		/// <summary>
		/// Allows text between empty tags to be included as nodes.
		/// Thus hosing the desirable behavior of nth-child.
		/// </summary>
		,KeepEmptyText = 1
		,ExpandScriptNodes = 2
	};

}
